import java.awt.Color;
import java.awt.Point;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.SwingUtilities;

public class GameBoard implements IBoard
{ 
  private int _rows, _cols;
  private ArrayList _players;
  private IPlayer[][] _matrix;
  private PlayerStats[] _stats;
  private int _currPlayer;
  private int _orientation = 0;
  private int _moves;
  private int _passes;
  private Color _boardColor;
  private int _gamesLeft;
  private boolean _hypothetical;
  private Point _lastMove;

  public GameBoard(int rows, int cols)
  {
    _rows = rows;
    _cols = cols;
    _players = new ArrayList();
    _gamesLeft = 0;
    _hypothetical = false;
  }

  private GameBoard(GameBoard copy)
  {
    _rows = copy._rows;
    _cols = copy._cols;
    _matrix = new IPlayer[copy._rows][copy._cols];
    for (int r = 0; r < _rows; r++)
    {
      for (int c = 0; c < _cols; c++)
      {
        _matrix[r][c] = copy._matrix[r][c];
      }
    }
    _players = new ArrayList(copy._players.size());
    _players.addAll(copy._players);
    _currPlayer = copy._currPlayer;
    _passes = copy._passes;
    _stats = null;	// We don't need this do we?
    _hypothetical = true;
  }

  private IPlayer getPlayer(Color c)
  {
    for (int i = 0; i < _players.size(); i++)
    {
      IPlayer p = (IPlayer)_players.get(i);
      if (p.getColor().equals(c))
        return p;
    }
    return null;
  }

  public void beginGame()
  {
    //		assert (!_hypothetical)
    //			: "A hypothetical board may not play an actual game.";
    //
    //		assert (_rows >= 4 && _cols >= 4)
    //			: "Invalid board size.  Must be at least 4x4";
    //		assert (_players.size() > 0)
    //			: "Must add players before beginning game.";

    _matrix = new IPlayer[_rows][_cols];
    //		_currPlayer = 0;
    _passes = 0;
    _lastMove = null;

    if (_currPlayer == 0)
      _orientation = (_orientation + 1) % 2;

    // At the beginning of the game, the center squares are flipped on.
    int rMid = _rows / 2;
    int cMid = _cols / 2;
    IPlayer p = (IPlayer)_players.get(_orientation);
    _matrix[rMid][cMid] = p;
    _matrix[rMid-1][cMid-1] = p;
    // REVIEW: This configuration doesn't support more than 2 players.
    p = (IPlayer)_players.get(_players.size() - 1 - _orientation);
    _matrix[rMid-1][cMid] = p;
    _matrix[rMid][cMid-1] = p;
    _moves = 4;

    beginTurn();
  }

  public void beginTurn()
  {
    int curr = _currPlayer;
    if (_stats != null)
      _stats[curr].startTurn();
    IPlayer player = ((IPlayer)_players.get(curr));
    player.beginTurn();

    // See if the player finished their turn yet.
    if (!(player instanceof HumanPlayer)
        && curr == _currPlayer)
    {
      // The human player is expected to make an asynchronous
      // move.  But computer players are expected to do their
      // full move during beginTurn().  One can imagine a network
      // player not doing that, but the warning is still useful.
      System.out.println("WARNING: Player '" + player.getName()
          + "' has not yet submitted a move.");
    }
  }

  public int getColCount()
  {
    return _cols;
  }

  public int getRowCount()
  {
    return _rows;
  }

  public IBoard hypotheticallyPlacePiece(Color playerColor, int row, int col)
  {
    GameBoard b = new GameBoard(this);
    IPlayer player = getPlayer(playerColor);
    b.placePiece(player, row, col);
    return b;
  }

  public boolean isValidMove(Color p, int row, int col)
  {
    if (row < 0 || row >= _rows || col < 0 || col >= _cols
        || _matrix[row][col] != null)
    {
      return false;
    }

    // A move is valid if it would flip at least one opposing piece.
    int flips = 0;
    flips += Math.max(0, walkVector(row, col, -1, 0, p));
    flips += Math.max(0, walkVector(row, col, -1, 1, p));
    flips += Math.max(0, walkVector(row, col, 0, 1, p));
    flips += Math.max(0, walkVector(row, col, 1, 1, p));
    flips += Math.max(0, walkVector(row, col, 1, 0, p));
    flips += Math.max(0, walkVector(row, col, 1, -1, p));
    flips += Math.max(0, walkVector(row, col, 0, -1, p));
    flips += Math.max(0, walkVector(row, col, -1, -1, p));
    return flips > 0;
  }

  public boolean placePiece(IPlayer player, int row, int col)
  {
    if (_hypothetical)
    {
      //			System.out.println(player.getName() + " is evaluating a move to (r" 
      //				+ (row+1) + ", c" + (col+1) + ")");
    }
    else
    {
      //			System.out.println(player.getName() + " wants to move to (r" 
      //				+ (row+1) + ", c" + (col+1) + ")");
    }

    // No matter what happens, make sure the board gets refreshed.
    IGui.get().redrawBoard();

    // Make sure the correct player is playing.
    if (isGameOver() || player != _players.get(_currPlayer))
    {
      //			assert (false) : "It is not " + player + "'s turn!!!!!";
      return false;
    }

    // Make sure the coordinates are valid.
    if (row < 0 || row >= _rows || col < 0 || col >= _cols)
      throw new IllegalArgumentException();

    // See if the spot is free.  If not, fail.
    IPlayer p = _matrix[row][col];
    if (p != null)
    {
      System.out.println("  Denied!  That space is not free!");
      return false;
    }

    // Make the move.
    _matrix[row][col] = player;

    // REVIEW: When a player has no valid moves, they need to be able to pass.

    int flips = 0;
    if (walkVector(row, col, -1, 0, player, false) > 0)		// up
      flips += walkVector(row, col, -1, 0, player, true);
    if (walkVector(row, col, -1, 1, player, false) > 0)		// up-right
      flips += walkVector(row, col, -1, 1, player, true);
    if (walkVector(row, col, 0, 1, player, false) > 0)		// right
      flips += walkVector(row, col, 0, 1, player, true);
    if (walkVector(row, col, 1, 1, player, false) > 0)		// down-right
      flips += walkVector(row, col, 1, 1, player, true);
    if (walkVector(row, col, 1, 0, player, false) > 0)		// down
      flips += walkVector(row, col, 1, 0, player, true);
    if (walkVector(row, col, 1, -1, player, false) > 0)		// down-left
      flips += walkVector(row, col, 1, -1, player, true);
    if (walkVector(row, col, 0, -1, player, false) > 0)		// left
      flips += walkVector(row, col, 0, -1, player, true);
    if (walkVector(row, col, -1, -1, player, false) > 0)		// up-left
      flips += walkVector(row, col, -1, -1, player, true);

    if (flips == 0)
    {
      // The move wasn't really valid, so revert.
      _matrix[row][col] = null;
      System.out.println("  Denied!  That move is not valid!");
      return false;
    }

    // The move was valid.
    // Once the matrix has been filled up, the game is over.
    _moves++;
    _passes = 0;
    _lastMove = new Point(col, row);

    if (!_hypothetical)
      player.endTurn();

    nextTurn();

    return true;
  }

  private int walkVector(int r0, int c0, int dR, int dC, Color player)
  {
    IPlayer p = getPlayer(player);
    if (p == null)
      return 0;
    return walkVector(r0, c0, dR, dC, p, false);
  }

  /**
   * Walk the matrix starting at r0,c0, moving in the direction dR,dC,
   * until a square is encountered with the same color as the player.
   * @return the number of squares traversed, not counting r0,c0 itself.
   * If an empty square or the edge of the board is encountered, return -1.
   * @param flip  if true, intervening squares are flipped to become this player.
   */
  private int walkVector(int r0, int c0, int dR, int dC, IPlayer player, boolean flip)
  {
    //		assert (dR >= -1 && dR <= 1 && dC >= -1 && dC <= 1);
    //		assert (dR != 0 || dC != 0);
    int r = r0 + dR;
    int c = c0 + dC;
    int steps = 0;

    while (r >= 0 && r < _rows && c >= 0 && c < _cols)
    {
      IPlayer p = _matrix[r][c];

      // Empty squares are treated like the edge of the board.
      if (p == null)
        break;
      // Squares already occupied by the player end the walk.
      // Return the number of opposing squares in between.
      if (p == player)
        return steps;
      // If we're flipping, flip.
      if (flip)
        _matrix[r][c] = player;
      // Walk onward.
      r += dR;
      c += dC;
      steps++;
    }

    // We ran into the edge of the board or an empty square before
    // encountering another square owned by this player.
    return -1;
  }

  public boolean pass(IPlayer player)
  {
    //		System.out.println(player.getName() + " wants to pass.");

    // No matter what happens, make sure the board gets refreshed.
    IGui.get().redrawBoard();

    // Make sure the correct player is playing.
    if (isGameOver() || player != _players.get(_currPlayer))
    {
      //			assert (false) : "It is not " + player + "'s turn!!!!!";
      return false;
    }

    if (hasValidMove(player))
    {
      // Player is not allowed to pass when they don't have to.
      return false;
    }

    // The pass was accepted.
    // Note that once everyone has passed (consecutively), the game is over.
    _passes++;
    _lastMove = null;

    if (!_hypothetical)
      player.endTurn();

    nextTurn();

    return true;
  }

  private boolean hasValidMove(IPlayer player)
  {
    // See if the player has any valid moves at all.
    for (int r = 0; r < getRowCount(); r++)
    {
      for (int c = 0; c < getColCount(); c++)
      {
        if (isValidMove(player.getColor(), r, c))
          return true;
      }
    }
    return false;
  }

  private void nextTurn()
  {
    IGui.get().showDropIndicator(null, -1, -1);

    if (isGameOver())
    {
      SwingUtilities.invokeLater(new Runnable()
          {
            public void run()
      {
        // Notify the next player.
        onGameOver(true);
      }
      });
      return;
    }

    if (_stats != null)
      _stats[_currPlayer].endTurn();

    _currPlayer = (_currPlayer + 1) % _players.size();

    if (!_hypothetical)
    {
      SwingUtilities.invokeLater(new Runnable()
          {
            public void run()
      {
        // Notify the next player.
        beginTurn();
      }
      });
    }
  }

  public boolean isGameOver()
  {
    return _moves >= _rows * _cols
      || _passes >= _players.size();
  }

  public Color playerAt(int row, int col)
  {
    if (_matrix == null)
      return null;

    if (row < 0 || row >= _rows || col < 0 || col >= _cols)
      throw new IllegalArgumentException();

    IPlayer p = _matrix[row][col];
    if (p == null)
      return null;
    return p.getColor();
  }

  public int playerScore(Color p)
  {
    if (_matrix == null)
      return 0;
    int score = 0;
    for (int r = 0; r < _rows; r++)
    {
      for (int c = 0; c < _cols; c++)
      {
        if (_matrix[r][c] != null && _matrix[r][c].getColor() == p)
          score++;
      }
    }
    return score;
  }

  public void addPlayer(IPlayer p, Color color)
  {
    //		assert (!_players.contains(p))
    //			: "May not add player (" + p + ") twice.";
    //		assert (getPlayer(color) == null)
    //			: "May not add another player with color " + color;

    _players.add(p);
    p.joinBoard(this, color);

    //		assert (getPlayer(p.getColor()) == null)
    //			: "Although this player was instructed to play as " + color 
    //			+ ".  They are playing as " + p.getColor() 
    //			+ " which is already claimed by another player.";
  }

  public Color getBoardColor()
  {
    if (_boardColor == null)
      _boardColor = new Color(0x66, 0x99, 0x33);
    if (_boardColor == null)
    {
      // Find a color that is unlike those of any of the players.
      Iterator iter = getPlayers();
      float[] floats = new float[3];
      int hue = 0, lum = 0, sat = 0;
      while (iter.hasNext())
      {
        Color pColor = ((IPlayerInfo)iter.next()).getColor();
        Color.RGBtoHSB(pColor.getRed(), pColor.getGreen(), pColor.getBlue(), floats);
        int h = (int)(floats[0] * 255.0);
        int l = (int)(floats[1] * 255.0);
        int s = (int)(floats[2] * 255.0);
        hue ^= h;
        lum = (lum + l) / 2;
        sat = (sat + s) / 2;
      }
      _boardColor = new Color(Color.HSBtoRGB(
            ((float)hue)/255.0f,
            ((float)lum)/255.0f,
            ((float)sat)/255.0f));
    }
    return _boardColor;
  }

  public Iterator getPlayers()
  {
    return new PlayerIterator();
  }

  private class PlayerIterator implements Iterator
  {
    /**
     * Counts from the index of the current player up to current + num-players.
     * That is, if it is currently player 2's turn out of 4 players, it
     * will count from 2 until 5.  Mod then makes the indeces valid.
     */
    private int _index;

    public PlayerIterator()
    {
      _index = _currPlayer;
    }

    public boolean hasNext()
    {
      return _index < _currPlayer + _players.size();
    }

    public Object next()
    {
      final IPlayer player = (IPlayer)_players.get(_index++ % _players.size());

      // Return a copy of the player info, so caller can neither change 
      // it, nor cast it to an IPlayer, nor use it to call placePiece().
      return new IPlayerInfo()
      {
        public String getName()	{ return player.getName(); }
        public Color getColor()	{ return player.getColor(); }
      };
    }

    public void remove()
    {
      //			assert (false) : "This iterator is read-only!";
    }
  }

  public void beginTournament(int numGames)
  {
    _gamesLeft = numGames;

    // Kick off the first game.
    onGameOver(false);
  }

  private void onGameOver(boolean addToStats)
  {
    if (!addToStats)
    {
      // Prior to the first game, initialize the stats.
      _stats = new PlayerStats[_players.size()];
      for (int i = 0; i < _players.size(); i++)
      {
        _stats[i] = new PlayerStats(((IPlayer)_players.get(i)).getName());
      }
    }
    else if (_stats != null)
    {
      // TODO: add this past game's stats to the running totals.
      int[] squares = new int[_players.size()];
      int most = -1;
      boolean tie = false;
      for (int i = 0; i < _players.size(); i++)
      {
        squares[i] = playerScore(((IPlayer)_players.get(i)).getColor());
        if (most < squares[i])
          most = squares[i];
        else if (most == squares[i])
          tie = true;
      }
      for (int i = 0; i < _players.size(); i++)
      {
        _stats[i].addStats(squares[i], most, tie);
      }
    }

    // Start another game if appropriate
    if (_gamesLeft-- > 0)
    {
      // Each game, a different player will start.
      // The board layout will remain the same though.
      _currPlayer = _gamesLeft % _players.size();
      beginGame();
    }
    IGui.get().redrawBoard();
  }

  public String getCurrentPlayerScore()
  {
    if (isGameOver())
      return null;
    IPlayer plyr = (IPlayer)_players.get(_currPlayer);
    return plyr.getName() + "'s turn (" + playerScore(plyr.getColor()) + ").";
  }

  public Color getCurrentPlayerColor()
  {
    if (isGameOver())
      return null;
    return ((IPlayer)_players.get(_currPlayer)).getColor();
  }

  public String getGameScore()
  {
    if (!isGameOver())
      return null;

    String score = "";
    for (int i = 0; i < _players.size(); i++)
    {
      IPlayer plyr = (IPlayer)_players.get(i);
      if (i >= 0)
        score += "   ";
      score += plyr.getName() + ": " + playerScore(plyr.getColor()) + ".";
    }
    return score;
  }

  public String getTournamentScore()
  {
    if (_stats == null)
      return null;

    String score = "";
    for (int i = 0; i < _stats.length; i++)
    {
      if (i >= 0)
        score += "   ";
      score += _stats[i].toString();
    }
    return score;
  }

  public Point lastMove()
  {
    return _lastMove;
  }
} 
